﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Resources;
using System.Reflection;
using System.Xml;
using System.IO;
using System.Xml.Linq;
using System.Windows;
using System.Windows.Input;
using System.Collections.ObjectModel;

namespace XPathAutoSuggest {
    /// <summary>
    /// ViewModel suitable for controlling a windows WPF form that works with a user
    /// to display XML and get XPath queries that work with it
    /// </summary>
    /// <remarks>
    /// <h2>Important Members</h2>
    /// <para>
    /// This ViewModel exposes, for binding, a number of commands, a string property called FormattedXML, a string property
    /// called XPathQuery, and an Observable collection called QuerySuggestions, plus several other members of lesser importance.
    /// </para>
    /// <list type="bullet">
    ///     <item>FormattedXML is suitable for binding to a text box where users can enter XML.</item>
    ///     <item>QuerySuggestions is suitable for binding to a data grid, it represents various XPath queries that can 
    ///            be used to select the node/tag user has highlighted in their XML.</item>
    ///     <item>XPathQuery is suitable for binding to a textbox, users can enter a query and ask the program to evaluate it</item>
    ///     <item>The various commands exposed by this class are suitable for performing the tasks suggested above, plus
    ///           validating/formatting users' XML, loading and saving XML to file, navigating to other Views in the app, etc.</item>
    /// </list>
    /// <h2>Class Inheritance and Interfaces</h2>
    /// <list type="bullet">
    ///     <item>Inherits from DependencyObject to work well with WPF</item>
    ///     <item>Implements IMainModelView to work with the Commands in MainWindowCommands</item>
    ///     <item>Implements IXPathToolVm to enable my CommandBase class to work with all ViewMOdels</item>
    ///     <item>Implements IFileIoCommands to satisfy SOLID design principle "Interface segregation principle",
    ///         the notion that “many client specific interfaces are better than one general purpose interface.</item>
    /// </list>
    /// <para>
    /// This application is written using the MVVM design pattern 
    /// http://blogs.msdn.com/b/dancre/archive/2006/10/11/datamodel-view-viewmodel-pattern-series.aspx". I also attempted to
    /// follow many of the principles laid-out in SOLID programming, refer to http://en.wikipedia.org/wiki/SOLID_%28object-oriented_design%29
    /// </para>
    /// </remarks>
    public class ModelView_MainWindow : DependencyObject, IMainModelView, IXPathToolVm, IFileIoCommands {
        #region Private Command Declarations
        private ICommand _EvaluateCommand;
        private ICommand _SuggestCommand;
        private ICommand _SaveCommand;
        private ICommand _NewCommand;
        private ICommand _OpenCommand;
        private ICommand _CloseCommand;
        private ICommand _FormatCommand;
        private ICommand _ShowLinksCommand;
        private ICommand _AboutCommand;
        private SaveAsCommand _SaveAsCommand;
        private ICommand _ValidateXmlCommand;
        #endregion

        #region Miscellaneous Member variables
        private IXPathUtility _XPathUtility;
        private IFileIoHelper _FileIOHelper;
        private string _FileName = string.Empty;
        private bool _IsDirty = false;
        private bool _ShowMessageBoxes;
        private ObservableCollection<QueryDescPair> _QuerySuggestions;

        /// <summary>An event which is raised when user indicates they wish to close the form</summary>
        public event CloseXmlFormEventHandler CloseXmlForm;
        #endregion

        #region Constructor
        /// <summary>
        /// Constructor takes an instance of the IXPathUtilty and the IFileIoHelper that it will utilize to
        /// perform its work.
        /// </summary>
        /// <param name="xPathHelper">An instance of the XML utility that can do significant work for us</param>
        /// <param name="fileIoHelper">Another helper utility for file IO</param>
        /// <remarks>
        /// By allowing the caller to pass us a different implementation of the utililty class to us, we introduce
        /// additional flexibility into how we parse and process the XML. That may make it easier for 
        /// improved utilities that have, for example superior suggestions
        /// </remarks>
        public ModelView_MainWindow(IXPathUtility xPathHelper, IFileIoHelper fileIoHelper) {
            //Display the sample XMl
            ResourceManager resMgr = new ResourceManager("XPathTool.Properties.Resources", Assembly.GetExecutingAssembly());
            string sampleXml = resMgr.GetString("sampleXml");
            FormattedXml = sampleXml;

            //Build the commands to bind to the View
            _EvaluateCommand = new EvaluateCommand(this);
            _SuggestCommand = new SuggestCommand(this);
            _SaveCommand = new SaveCommand(this);
            _NewCommand = new NewCommand(this);
            _OpenCommand = new OpenCommand(this);
            _CloseCommand = new CloseCommand(this);
            _FormatCommand = new FormattCommand(this);
            _ShowLinksCommand = new ShowLinksCommand(this);
            _AboutCommand = new AboutCommand(this);
            _SaveAsCommand = new SaveAsCommand(this);
            _ValidateXmlCommand = new ValidateXmlCommand(this);

            _QuerySuggestions = new ObservableCollection<QueryDescPair>();
            _IsDirty = false;
            _ShowMessageBoxes = true;
            _XPathUtility = xPathHelper;
            _FileIOHelper = fileIoHelper;
        }
        #endregion

        #region Command Properties
        /// <summary>
        /// Command fired when user evaluates some XPath
        /// </summary>
        public ICommand EvaluateCommand {
            get { return _EvaluateCommand; }
        }

        /// <summary>
        /// Fired when user wants a suggestion for XPath
        /// </summary>
        public ICommand SuggestCommand {
            get { return _SuggestCommand; }
        }

        /// <summary>
        /// Fired by the menu item to save
        /// </summary>
        public ICommand SaveCommand {
            get { return _SaveCommand; }
        }

        /// <summary>
        /// Fired by the New menu item
        /// </summary>
        public ICommand NewCommand {
            get { return _NewCommand; }
        }

        /// <summary>
        /// Fired by the menu item Open
        /// </summary>
        public ICommand OpenCommand {
            get { return _OpenCommand; }
        }

        /// <summary>
        /// Fired by the menu item close
        /// </summary>
        public ICommand CloseCommand {
            get { return _CloseCommand; }
        }

        /// <summary>
        /// Fired when user wants to format their XML
        /// </summary>
        public ICommand FormatCommand {
            get { return _FormatCommand; }
        }

        /// <summary>
        /// Fired when user wants to see the page with help links
        /// </summary>
        public ICommand ShowLinksCommand {
            get { return _ShowLinksCommand; }
        }

        /// <summary>
        /// Fired when user wants to see the page with about information
        /// </summary>
        public ICommand AboutCommand {
            get { return _AboutCommand; }
        }

        /// <summary>
        /// Fired when user wants to save their XML with new file name
        /// </summary>
        public SaveAsCommand SaveAsCommand {
            get { return _SaveAsCommand; }
        }

        /// <summary>
        /// A command to fire to validate users XML
        /// </summary>
        public ICommand ValidateXmlCommand {
            get { return _ValidateXmlCommand; }
        }
        #endregion

        #region Dependency Properties for Binding
        /// <summary>
        /// Gets/sets the XML user enters
        /// </summary>
        public string FormattedXml {
            get { return (string)GetValue(FormattedXmlProperty); }
            set {
                SetValue(FormattedXmlProperty, value);
                _IsDirty = true;
            }
        }
        /// <summary>Backing store for the FormattedXml</summary>
        public static DependencyProperty FormattedXmlProperty = DependencyProperty.Register("FormattedXml", typeof(string),
                                typeof(ModelView_MainWindow), new UIPropertyMetadata(""));

        /// <summary>
        /// Gets/sets statusbar message
        /// </summary>
        public string StatusInfo {
            get { return (string)GetValue(StatusInfoProperty); }
            set { SetValue(StatusInfoProperty, value); }
        }

        /// <summary>Backing store for the StatusInfo</summary>
        public static readonly DependencyProperty StatusInfoProperty =
            DependencyProperty.Register("StatusInfo", typeof(string), typeof(ModelView_MainWindow), new UIPropertyMetadata(""));

        /// <summary>
        /// Location within FormattedXml where usres' selected text begins
        /// </summary>
        public int SelectionStart {
            get { return (int)GetValue(SelectionStartProperty); }
            set { SetValue(SelectionStartProperty, value); }
        }

        /// <summary>Backing store for the Selection start</summary>
        public static readonly DependencyProperty SelectionStartProperty =
            DependencyProperty.Register("SelectionStart", typeof(int), typeof(ModelView_MainWindow), new UIPropertyMetadata(0));


        /// <summary>
        /// Set after user evaluates their XPath query, either 'No hits' or else the node
        /// which matches their query
        /// </summary>
        public string XPathResults {
            get { return (string)GetValue(XPathResultsProperty); }
            set { SetValue(XPathResultsProperty, value); }
        }
        /// <summary>Backing store for the XPath results</summary>
        public static DependencyProperty XPathResultsProperty = DependencyProperty.Register("XPathResults", typeof(string),
                                typeof(ModelView_MainWindow), new UIPropertyMetadata(""));

        /// <summary>
        /// Represents the text within the full XML that user wishes to generate suggestions 
        /// for finding via Xpath query
        /// </summary>
        public string SelectedXml {
            get { return (string)GetValue(SelectedXmlProperty); }
            set { 
                SetValue(SelectedXmlProperty, value);
                _QuerySuggestions.Clear();
            }
        }
        /// <summary>
        /// Backing store for the Selected XML
        /// </summary>
        public static DependencyProperty SelectedXmlProperty = DependencyProperty.Register("SelectedXml", typeof(string),
                                typeof(ModelView_MainWindow), new UIPropertyMetadata(""));

        /// <summary>
        /// The query to evaluate, whether supplied by user or suggested by XmlHelper
        /// </summary>
        public string XPathQuery {
            get { return (string)GetValue(XPathQueryProperty); }
            set { SetValue(XPathQueryProperty, value); }
        }
        /// <summary>Backing store for the XPathQuery</summary>
        public static readonly DependencyProperty XPathQueryProperty =
            DependencyProperty.Register("XPathQuery", typeof(string), typeof(ModelView_MainWindow), new UIPropertyMetadata(""));

        #endregion

        #region Bindable collection
        /// <summary>
        /// Contains the list of suggestions
        /// </summary>
        public ObservableCollection<QueryDescPair> QuerySuggestions {
            get { return _QuerySuggestions; }
        }
        #endregion

        #region CanExecuteMethods
        /// <summary>
        /// Allow users to save if there is any XML in the textbox
        /// </summary>
        /// <returns></returns>
        public bool SaveCommandCanExecute() {
            return !string.IsNullOrEmpty(this.FormattedXml);
        }

        /// <summary>
        /// Users can always make a new XML document
        /// </summary>
        /// <returns></returns>
        public bool NewCanExecute() {
            return true;
        }

        /// <summary>
        /// Users can always open a new file
        /// </summary>
        /// <returns></returns>
        public bool OpenCommandCanExecute() {
            return true;
        }

        /// <summary>
        /// Users can ask for a suggestion if they have somehow supplied XML and then selected
        /// a substring within it
        /// </summary>
        /// <returns></returns>
        public bool SuggestCanExecute() {
            return !string.IsNullOrEmpty(SelectedXml);
        }

        /// <summary>
        /// Users can format their XML if it is not empty
        /// </summary>
        /// <returns></returns>
        public bool FormatCommandCanExecute() {
            return !string.IsNullOrEmpty(FormattedXml);
        }

        /// <summary>
        /// Users can always close
        /// </summary>
        /// <returns></returns>
        public bool CloseCommandCanExecute() {
            return true;
        }

        /// <summary>
        /// Users can always show the form with the links
        /// </summary>
        /// <returns></returns>
        public bool ShowLinksCommandCanExecute() {
            return true;
        }

        /// <summary>
        /// Controls whether the AboutCommand can fire
        /// </summary>
        /// <returns>True</returns>
        public bool AboutCommandCanExecute() {
            return true;
        }

        /// <summary>
        /// Users can save if their XML is present
        /// </summary>
        /// <returns></returns>
        public bool SaveAsCommandCanExecute() {
            return this.FormattedXml.Length > 0;
        }

        /// <summary>
        /// Whether the Evaluate command can execute
        /// </summary>
        /// <returns>True if user has supplied some XPath or if there is a suggestion displayed</returns>
        public bool EvaluateCommandCanExecute() {
            return QuerySuggestions.Count > 0 ||
                   !string.IsNullOrEmpty(XPathQuery);
        }

        /// <summary>
        /// Whether the ValidateXml command can fire
        /// </summary>
        /// <returns>True/false</returns>
        public bool ValidateXmlCommandCanExecute() {
            return FormattedXml.Length > 0;
        }
        #endregion

        #region Command Execute methods
        /// <summary>
        /// Save users' XMl to a file name they provide
        /// </summary>
        public void SaveCommandExecute() {
            if (string.IsNullOrEmpty(_FileName)) {
                _FileName = _FileIOHelper.GetSaveFileName();
            }
            if (!string.IsNullOrEmpty(_FileName)) {
                string newStatus = string.Empty;
                _FileIOHelper.SaveToFile(FormattedXml, _FileName, ref _IsDirty, ref newStatus);
                StatusInfo = newStatus;
            }
        }

        /// <summary>
        /// Save after prompting for a new file name
        /// </summary>
        public void SaveAsCommandExecute() {
            _FileName = _FileIOHelper.GetSaveFileName();
            if (!string.IsNullOrEmpty(_FileName)) {
                string newStatus = string.Empty;
                _FileIOHelper.SaveToFile(FormattedXml, _FileName, ref _IsDirty, ref newStatus);
                StatusInfo = newStatus;
            }
        }

        /// <summary>
        /// Opens file and displays the contents
        /// </summary>
        public void OpenCommandExecute() {
            _FileName = _FileIOHelper.GetOpenFileName();

            string xml = _FileIOHelper.ReadFile(_FileName);
            string msg = _XPathUtility.ValidateXml(xml);

            if (string.IsNullOrEmpty(msg)) {
                StatusInfo = "Loaded file '" + _FileName + "'";
            } else {
                StatusInfo = msg;
                if (_ShowMessageBoxes)
                    MessageBox.Show("Your file is not well-formatted XML, the error message is\r\n\r\n" +
                        msg, "Bad XML", MessageBoxButton.OK, MessageBoxImage.Error);
            }
            this.FormattedXml = xml;
            _IsDirty = false;

        }

        /// <summary>
        /// Gets rid of old XMl, starts with fresh slate
        /// </summary>
        public void NewExecute() {
            _FileName = string.Empty;
            _IsDirty = false;
            FormattedXml = string.Empty;
        }

        /// <summary>
        /// Runs users' query and puts the results into our XPathResults property
        /// </summary>
        public void EvaluateExecute() {
            EvaluateExecute(XPathQuery);
        }

        /// <summary>
        /// Attempts to generate XPATH queries that find the text selected by user
        /// </summary>
        public void SuggestExecute() {
            _QuerySuggestions.Clear();
            XPathQuery = string.Empty;
            XmlDocument xDoc = new XmlDocument();
            StringReader sr = new StringReader(FormattedXml);
            try {
                xDoc.Load(sr);

                ObservableCollection<QueryDescPair> result = _XPathUtility.GetXPathQueries(FormattedXml, SelectedXml, SelectionStart);
                foreach (QueryDescPair p in result) {
                    _QuerySuggestions.Add(p);
                }
                StatusInfo = string.Format("Generated {0} queries to find '{1}' at position {2}",
                                           _QuerySuggestions.Count, SelectedXml, SelectionStart);
            
            } catch (XmlException x) {
                if (_ShowMessageBoxes)
                    MessageBox.Show("Error parsing your XML: \r\n" +
                                    "Line:     " + x.LineNumber.ToString() + "\r\n" +
                                    "Position: " + x.LinePosition.ToString() + "\r\n" +
                                    "Message:  " + x.Message, "Invalid XML", MessageBoxButton.OK,
                                    MessageBoxImage.Error);
            } 
        }

        /// <summary>
        /// Lines-up, indents the XML entered by user
        /// </summary>
        public void FormatCommandExecute() {
            try {
                FormattedXml = XElement.Parse(FormattedXml, LoadOptions.PreserveWhitespace).ToString();
                StatusInfo = string.Empty;
            } catch (Exception ex) {
                StatusInfo = ex.Message;
            }
        }

        /// <summary>
        /// Checks users XML and displays results as message box and status
        /// </summary>
        public void ValidateXmlCommandExecute() {
            string msg = _XPathUtility.ValidateXml(FormattedXml);
            if (_ShowMessageBoxes) {
                if (string.IsNullOrEmpty(msg)) {
                    MessageBox.Show("No problems found", "Success", MessageBoxButton.OK, MessageBoxImage.Information);
                } else {
                    MessageBox.Show("Problems found: \r\n" + msg, "Some Issues Found", MessageBoxButton.OK, MessageBoxImage.Exclamation);
                }
            }
            StatusInfo = msg;
        }

        /// <summary>
        /// Displays the form with links to XPath documentation
        /// </summary>
        public void ShowLinksCommandExecute() {
            HelpLinks lf = new HelpLinks();
            lf.ShowDialog();
        }

        /// <summary>
        /// Runs the users' XPath query against FormattedXml and exposes the results as XPathResults
        /// </summary>
        /// <param name="inXpathQuery">Query to attempt</param>
        public void EvaluateExecute(string inXpathQuery) {
            XmlDocument xDoc = new XmlDocument();
            StringReader sr = new StringReader(FormattedXml);
            try {
                xDoc.Load(sr);
                XmlNodeList hitList = xDoc.SelectNodes(inXpathQuery);
                if (hitList == null || hitList.Count == 0) {
                    XPathResults = "No hits";
                } else {
                    if (hitList.Count == 1)
                        XPathResults = hitList[0].InnerText;
                    else
                        XPathResults = hitList[0].InnerText + " (" + hitList.Count.ToString() + " total hits)";
                }
            } catch (XmlException xEx) {
                XPathResults = xEx.Message;
                Console.WriteLine(xEx.LineNumber.ToString());
                Console.WriteLine(xEx.LinePosition.ToString());
            } catch (Exception ex) {
                XPathResults = ex.Message;
            }
            XPathQuery = inXpathQuery;
        }

        /// <summary>
        /// Closes the application after asking user if they want to save (when dirty)
        /// </summary>
        public void CloseCommandExecute() {
            if (_IsDirty) {
                MessageBoxResult res = MessageBox.Show("Changes will be lost, do you want to save them now?",
                            "Click Yes to Save",
                            MessageBoxButton.YesNoCancel, MessageBoxImage.Question);
                if (res == MessageBoxResult.Cancel)
                    return;
                else if (res == MessageBoxResult.Yes)
                    SaveCommandCanExecute();
            }
            if (CloseXmlForm != null) {
                CloseXmlForm(this, EventArgs.Empty);
            }
        }

        /// <summary>
        /// Launches the dialog to display 'about' information
        /// </summary>
        public void AboutCommandExecute() {
            AboutForm abt = new AboutForm();
            abt.ShowDialog();
        }
        #endregion

        #region Miscellaneous Properties
        /// <summary>
        /// The file name used to read/write the XML used for XPath queries
        /// </summary>
        public string FileName {
            get { return _FileName; }
            set { _FileName = value; }
        }

        /// <summary>
        /// Whether to use messageboxes for user feedback
        /// </summary>
        public bool ShowMessageBoxes {
            get { return _ShowMessageBoxes; }
            set { _ShowMessageBoxes = value; }
        }
        #endregion
    }
}

